/* 
 * StikiWeb Wiki - a Wiki for Hierarchical Organizations
 * $Id: PageManager.java,v 1.6 2006/07/20 21:09:24 biglee Exp $
 * 
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the StikiWeb Wiki.
 *
 * The Initial Developer of the Original Code is Lee Haslup (biglee@haslups.com).
 * Portions created by the Initial Developer are Copyright (C) 2004
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * ***** END LICENSE BLOCK ***** 
 */
package org.stikiweb.contentMagnager;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;

import org.stikiweb.translations.StikiWebTranslator;

/**
 * @author Allen L (BigLee) Haslup
 *
 */
public class PageManager {

	// Singleton class -- do not instantiate except via solo method.
	private PageManager() {};
	private static PageManager solo;
	public static synchronized PageManager solo() {
		if (solo == null) {
			solo = new PageManager();
		}
		return solo;
	}
	
	protected HashMap pageCache = new HashMap();
	protected List pageAging = new LinkedList();
	protected static final int cacheMax = 250;
	protected HashMap pageChangeListeners = new HashMap();
	
	protected synchronized String cacheGet(String name) {
		// get the value
		String page = (String)pageCache.get(name);
		if(page==null) return null;
		// make this the youngest page since it has been referenced
		pageAging.remove(name);
		pageAging.add(name);
		return page;
	}
	protected synchronized void cachePut(String name, String page) {
		// if the page is already cached, replace it and make it youngest
		if(pageCache.containsKey(name)) {
			pageAging.remove(name);
			pageAging.add(name);
			pageCache.put(name,page);
		}
		// new page to be added in cache
		else {
			// if the cache is full remove the oldest page
			if(pageCache.size()>=cacheMax) {
				String oldestPageName = (String)pageAging.remove(0);
				pageCache.remove(oldestPageName);
			}
			// put the new page in the cache as the youngest
			pageCache.put(name,page);
			pageAging.add(name);
		}
	}
	
	protected synchronized void cacheDrop(String name) {
		pageCache.remove(name);
		pageAging.remove(name);
	}
	
	protected synchronized void addPageChangeListener(String page, PageChangeListener listener) {
		Vector currentListeners = (Vector)pageChangeListeners.get(page);
		if(currentListeners==null) {
			currentListeners = new Vector();
			pageChangeListeners.put(page,currentListeners);
		}
		currentListeners.add(listener);
	}
	
	protected synchronized void dropPageChangeListener(String page, PageChangeListener listener) {
		Vector currentListeners = (Vector)pageChangeListeners.get(page);
		if(currentListeners==null) return;
		Vector newListeners = new Vector();
		Iterator it = currentListeners.iterator();
		while(it.hasNext()) {
			PageChangeListener l = (PageChangeListener)it.next();
			if(l!=listener) {
				newListeners.add(l);
			}
		}
		if(newListeners.isEmpty()) {
			pageChangeListeners.remove(page);
		}
		else {
			pageChangeListeners.put(page,newListeners);
		}
	}

	protected void notifyListenersOfChange(String page) {
		Vector listeners = (Vector)pageChangeListeners.get(page);
		if(listeners==null) return;
		Iterator it = listeners.iterator();
		while(it.hasNext()) {
			((PageChangeListener)it.next()).pageChanged(page);
		}
	}

	// Start over (releases old cache)
	public static void resetPM() {
		if(solo!=null) {
			solo.pageAging = null;
			solo.pageCache = null;
			solo.pageChangeListeners = null;
			solo=null;
		}
	}

	public boolean pageExists(String name) {
		return PageInfoNode.forName(name)!=null;
	}

	public String getPage(String name)
		throws PageManagerException {
		// the null page (wiki base) is always empty.
		if(name==null || name.length()==0 || name.equals("..")){
			return "";
		}
		// determine the file name.
		String fileName = StikiWebTranslator.convertName(name);
		// return cached value, if there is one.
		String cachedPage = cacheGet(fileName);
		if(cachedPage!=null) {
			return cachedPage;
		}
		try {
			// see if we can open the file.
			File infile = new File(StikiWebContext.solo().wikiBase+fileName);
			if(!infile.exists() || infile.isDirectory()) {
				throw new PageManagerException("File " + name + " not found.");
			}
			FileInputStream in = new FileInputStream(infile);
			//read it in
			StringWriter sw = new StringWriter();
			try {
				int c = in.read();
				while (c >= 0) {
					sw.write(c);
					c = in.read();
				}
			} catch (IOException e) {
				e.printStackTrace();
				throw new PageManagerException(
					"I/O error reading file " + name + ".");
			}
			// cache it
			String pageIn = sw.toString();
			cachePut(fileName,pageIn);
			// and return it
			return pageIn;
		} catch (FileNotFoundException e1) {
			throw new PageManagerException("File " + name + " not found.");
		}
	}
	
	public Vector getConfigurationTable(String name) {
		Vector retval = new Vector();
		try {
			// read the page one line at a time
			String page = getPage(name);
			StringReader sr = new StringReader(page);
			BufferedReader br = new BufferedReader(sr);
			for(String line = br.readLine(); line!=null; line=br.readLine()) {
				// only process lines that start with three vertical bars (simple table rows)
				line=line.trim();
				if(line.startsWith("|||")) {
					// each row is an entry in the top-level vector
					Vector entry = new Vector();
					retval.add(entry);
					// consume the line as fields with three vertical bars as separators
					while(line.length()>0) {
						line=line.substring(3);
						int sep=line.indexOf("|||");
						if(sep<0) sep=line.length();
						String field = line.substring(0,sep);
						entry.add(field);
						line=line.substring(sep);
					}
				}
			}
		} catch (PageManagerException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return retval;
	}
	
	public void saveConfigurationTable (String name, Vector newContent) throws PageManagerException, IOException {
		// Buffer to accumulate new page text
		StringBuffer sb = new StringBuffer();
		// If the page already exists, copy any text above the auto-table
		if(pageExists(name)) {
			String oldText = getPage(name);
			StringReader sr = new StringReader(oldText);
			BufferedReader br = new BufferedReader(sr);
			for(String line=br.readLine();line!=null&!line.startsWith("||");line=br.readLine()){
				sb.append(line+"\n");
			}
		}
		// then add the new content as an auto-table
		Iterator lines = newContent.iterator();
		while(lines.hasNext()) {
			Iterator fields = ((Vector)lines.next()).iterator();
			while(fields.hasNext()) {
				String field = (String)fields.next();
				if(field==null) field = "";
				sb.append("|||"+field);
			}
			sb.append("\n");
		}
		// and save the page
		savePage(name,sb.toString());
	}
	
	public void savePage(String name, String markup)
		throws PageManagerException
	{
		String fileName = StikiWebTranslator.convertName(name);
		String path = StikiWebContext.solo().wikiBase;
		File outFile = new File(path,fileName);
		if(outFile.exists()) {
			// file exists, make sure we can write it.
			if(!outFile.canWrite()) {
				throw new PageManagerException("File "+name+" not writable.");
			} 
		}
		// file does not exist, make sure we can create it
		else {
			File parent = outFile.getParentFile();
			if(!parent.exists()) {
				if(!parent.mkdirs()) {
					throw new PageManagerException("Cannot create directory "+ parent + ".");
				}
			}
			try {
				if(!outFile.createNewFile()) {
					throw new PageManagerException("Cannot Create file" + outFile + ".");
				}
			} catch (IOException e1) {
				e1.printStackTrace();
				throw new PageManagerException("Cannot Create file" + outFile + " - I/O error.");
			}
		}
		// file is good to go at this point.
		try {
			FileOutputStream fo = new FileOutputStream(outFile);
			StringReader sr=new StringReader(markup);
			for(int c = sr.read(); c>=0; c=sr.read()) {
				fo.write(c);
			}
			fo.flush();
			// file written to disk successfully, update the cache
			cachePut(fileName,markup);
			// add the page to the PageInfoNode tree
			PageInfoNode.addPage(name);
			// and notify any listeners
			notifyListenersOfChange(name);
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			throw new PageManagerException("I/O Error writing file "+fileName+".\n");
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			throw new PageManagerException("I/O Error writing file "+fileName+".\n");
		}		
	}
	
	public void deletePage(String name)
	throws PageManagerException
	{
		String fileName = StikiWebTranslator.convertName(name);
		String path = StikiWebContext.solo().wikiBase;
		
		// the file we are trying to delete.
		File obsoleteFile = new File(path,fileName);
		
		// should exist and be an ordinary file
		if(!obsoleteFile.exists() || !obsoleteFile.isFile()) {
			// file doesn't exist
			throw new PageManagerException("File "+name+" not found.");
		}
		
		// try to delete the file.
		if(obsoleteFile.delete()) {
			
			// file is gone.  Tidy up a bit.  First, drop it from the cache...
			cacheDrop(fileName);
			
			// then from the Page Info Node data structure.
			PageInfoNode.dropPage(name);
			
			// and remove the page spec
			try {
				PermissionManager.solo().removePageSpec(name);
			} catch (PageManagerException e) {
				// not exactly clear what to do about this if it happens.
				e.printStackTrace();
			} catch (IOException e) {
				// not exactly clear what to do about this if it happens.
				e.printStackTrace();
			}
			
			
			// if this page had a directory for children/attachments try and delete it
			// (but don't die trying because the directory may contain a CVS directory
			// which will prevent it from being deleted.)  
			String attDirName = fileName.substring(0,fileName.length()-5);
			File attDir = new File(path,attDirName);
			if(attDir.isDirectory()) {
				attDir.delete();
			}
		}
		else {
			throw new PageManagerException("I/O Error Deleting "+ fileName);
		}
	}
	public Vector listChildren(String name, boolean includeAttachments){
		
		//vector to accumulate returned page names.
		Vector retval = new Vector();
		
		PageInfoNode theNode = PageInfoNode.forName(name);
		if(theNode==null) {
			return retval;
		}
		
		Iterator it = theNode.getChildren().values().iterator();
		while(it.hasNext()) {
			PageInfoNode theChild = (PageInfoNode) it.next();
			retval.add(theChild.getName());
		}
		
		if(!includeAttachments) {
			return retval;
		}
		
		retval.addAll(theNode.getAttachments().values());
		
		return retval;
	}
	
	public Vector listAttachments(String name) {
		
		//vector to accumulate returned page names.
		Vector retval = new Vector();
		
		PageInfoNode theNode = PageInfoNode.forName(name);
		if(theNode==null) {
			return retval;
		}
		
		retval.addAll(theNode.getAttachments().values());
		
		return retval;
	}
}
